use base64::Engine;
use blake2::Digest;
use rand::rngs::OsRng;
use rsa::pkcs8::{
    DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _, EncodePublicKey,
};
use rsa::{RsaPrivateKey, RsaPublicKey};

use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeSet;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use totp_rs::TOTP;

use crate::error::Error;
use crate::Config;

const KEY_INDEX: &str = "keys/index.json";
const MESSAGE_INDEX: &str = "safebox/index.json";

struct PlainMessage {
    name: String,
    body: String,
    name_b2sum: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SecretMessage {
    name: String,
    body: String,
    name_b2sum: String,
}

struct PubKey {
    name: String,
    key: RsaPublicKey,
}

struct SecKey {
    // name: String,
    key: RsaPrivateKey,
}

pub struct DataBox {
    config: Config,
}

impl DataBox {
    pub fn new(config: Config) -> Self {
        Self { config }
    }

    fn message_file_path(&self, keyname: &str, message_name_b2sum: &str) -> Result<PathBuf, Error> {
        Ok(self
            .data_dir()?
            .join("safebox")
            .join(keyname)
            .join(format!("{}.json", message_name_b2sum)))
    }

    fn encrypt(&self, pub_key: &PubKey, plain: &PlainMessage) -> Result<SecretMessage, Error> {
        let name = pub_key.key.encrypt(
            &mut OsRng,
            rsa::Pkcs1v15Encrypt,
            &plain.name.to_owned().into_bytes(),
        )?;

        let body = pub_key.key.encrypt(
            &mut OsRng,
            rsa::Pkcs1v15Encrypt,
            &plain.body.to_owned().into_bytes(),
        )?;

        Ok(SecretMessage {
            name: base64::engine::general_purpose::STANDARD.encode(name),
            body: base64::engine::general_purpose::STANDARD.encode(body),
            name_b2sum: plain.name_b2sum.clone(),
        })
    }

    fn decrypt(&self, sec_key: &SecKey, secret: &SecretMessage) -> Result<PlainMessage, Error> {
        let name = sec_key.key.decrypt(
            rsa::Pkcs1v15Encrypt,
            &base64::engine::general_purpose::STANDARD.decode(secret.name.clone())?,
        )?;

        let body = sec_key.key.decrypt(
            rsa::Pkcs1v15Encrypt,
            &base64::engine::general_purpose::STANDARD.decode(secret.body.clone())?,
        )?;

        Ok(PlainMessage {
            name: String::from_utf8(name)?,
            body: String::from_utf8(body)?,
            name_b2sum: secret.name_b2sum.clone(),
        })
    }

    fn insert_message_for_key(
        &self,
        pub_key: &PubKey,
        plain: &PlainMessage,
        force: bool,
    ) -> Result<String, Error> {
        let path = self.message_file_path(&pub_key.name, &plain.name_b2sum)?;

        if !force && path.is_file() {
            return Ok(plain.name_b2sum.clone());
        }

        let secret = self.encrypt(pub_key, plain)?;

        if let Some(parent) = path.parent() {
            if !parent.is_dir() {
                let _ = std::fs::create_dir_all(parent);
            }
        }

        let writer = std::fs::File::create(path)?;
        serde_json::to_writer_pretty(writer, &secret)?;

        Ok(secret.name_b2sum)
    }

    fn data_dir(&self) -> Result<&Path, Error> {
        if let Some(path) = &self.config.data_dir {
            Ok(path.as_path())
        } else {
            eprintln!("Missing flag --data-dir=DATA_DIR");
            Err(Error)
        }
    }

    pub fn show(&self, key: Option<String>, name: String, totp: bool) -> Result<(), Error> {
        let name_b2sum = b2sum(&name);

        let messages = load_list(&self.data_dir()?.join(MESSAGE_INDEX)).map_err(|_| {
            eprintln!("Failed to load message list");
            Error
        })?;

        if !messages.contains(&name_b2sum) {
            eprintln!("Invalid message name: {name}");
            return Err(Error);
        }

        let key_name = self.resolve_key_name(key)?;
        let sec_key = self.load_sec_key(&key_name)?;

        let secret = load_secret_message(&self.message_file_path(&key_name, &name_b2sum)?)?;
        let plain = self.decrypt(&sec_key, &secret)?;

        if totp {
            show_totp(&plain.body);
        } else {
            println!("{}", plain.body);
        }

        Ok(())
    }

    pub fn generate_key(&self, name: String) -> Result<(), Error> {
        self.create_key_pair(&name)?;

        let mut keys = load_list(&self.data_dir()?.join(KEY_INDEX)).map_err(|_| {
            eprintln!("Failed to load key list");
            Error
        })?;

        keys.insert(name);

        dump_list(keys, &self.data_dir()?.join(KEY_INDEX))?;

        self.sync(None)?;

        Ok(())
    }

    pub fn speech(&self, key: Option<String>) -> Result<(), Error> {
        let messages = load_list(&self.data_dir()?.join(MESSAGE_INDEX)).map_err(|_| {
            eprintln!("Failed to load message list");
            Error
        })?;

        let key = self.resolve_key_name(key)?;
        let keys = load_list(&self.data_dir()?.join(KEY_INDEX))?;

        let sec_key = self.load_sec_key(&key).map_err(|_| {
            eprintln!("Failed to load secret key: {key}");
            Error
        })?;

        for b2sum in messages.iter() {
            let path = self.message_file_path(&key, b2sum)?;

            if !path.is_file() {
                continue;
            }

            let message = load_secret_message(&path)?;
            let plain = self.decrypt(&sec_key, &message)?;

            for key in &keys {
                let pub_key = self.load_pub_key(key)?;

                self.insert_message_for_key(&pub_key, &plain, false)?;
            }
        }

        self.sync(None)?;

        Ok(())
    }

    pub fn list(&self, key: Option<String>) -> Result<(), Error> {
        let messages = load_list(&self.data_dir()?.join(MESSAGE_INDEX)).map_err(|_| {
            eprintln!("Failed to load message list");
            Error
        })?;

        let key = self.resolve_key_name(key)?;

        let sec_key = self.load_sec_key(&key).map_err(|_| {
            eprintln!("Failed to load secret key: {key}");
            Error
        })?;

        let mut names = vec![];

        for b2sum in messages.iter() {
            let path = self.message_file_path(&key, b2sum)?;

            let message = load_secret_message(&path)?;

            let name = sec_key.key.decrypt(
                rsa::Pkcs1v15Encrypt,
                &base64::engine::general_purpose::STANDARD.decode(message.name)?,
            )?;

            names.push(name);
        }

        names.sort_unstable();
        for name in names {
            println!("{}", String::from_utf8(name)?);
        }

        eprintln!("=> {} message(s) in total", messages.len());

        Ok(())
    }

    fn resolve_key_name(&self, key: Option<String>) -> Result<String, Error> {
        if let Some(key) = key {
            return Ok(key);
        }

        if let Some(key) = &self.config.default_key {
            return Ok(key.clone());
        }

        eprintln!("Need secret key name, use -kNAME|--key=NAME or --default-key=NAME, see -h,--help for help");
        Err(Error)
    }

    pub fn insert(&self, name: String, force: bool) -> Result<(), Error> {
        let keys = load_list(&self.data_dir()?.join(KEY_INDEX))?;
        let mut messages = load_list(&self.data_dir()?.join(MESSAGE_INDEX))?;

        let name_b2sum = b2sum(&name);

        if !force && messages.contains(&name_b2sum) {
            eprintln!("Duplicated message name: {name}");
            return Err(Error);
        }

        if unsafe { stdout_isatty() && stdin_isatty() } {
            print!("Insert body for {}> ", name);
            let _ = std::io::stdout().flush();
        }

        let mut buf = String::new();
        std::io::stdin().read_line(&mut buf)?;
        let message = buf.trim().to_owned();

        for key in keys {
            let pub_key = self.load_pub_key(&key)?;
            messages.insert(self.insert_message_for_key(
                &pub_key,
                &PlainMessage {
                    name: name.to_owned(),
                    body: message.to_owned(),
                    name_b2sum: name_b2sum.clone(),
                },
                force,
            )?);
        }

        dump_list(messages, &self.data_dir()?.join(MESSAGE_INDEX))?;

        self.sync(None)?;

        Ok(())
    }

    fn load_sec_key(&self, name: &str) -> Result<SecKey, Error> {
        let path = self.sec_key_path(name)?;

        let key = RsaPrivateKey::from_pkcs8_pem(&std::fs::read_to_string(path)?)?;

        Ok(SecKey { key })
    }

    fn sec_key_path(&self, name: &str) -> Result<PathBuf, Error> {
        Ok(self
            .data_dir()?
            .join("keys")
            .join(format!("{name}.sec.txt")))
    }

    fn load_pub_key(&self, name: &str) -> Result<PubKey, Error> {
        let path = self.pub_key_path(name)?;

        let key = RsaPublicKey::from_public_key_pem(&std::fs::read_to_string(path)?)?;

        Ok(PubKey {
            key,
            name: name.to_owned(),
        })
    }

    fn pub_key_path(&self, name: &str) -> Result<PathBuf, Error> {
        Ok(self
            .data_dir()?
            .join("keys")
            .join(format!("{name}.pub.txt")))
    }

    fn create_key_pair(&self, name: &str) -> Result<(), Error> {
        let mut rng = OsRng;
        let bits = 4096;

        let sec_key = RsaPrivateKey::new(&mut rng, bits)?;
        let pub_key = RsaPublicKey::from(&sec_key);

        std::fs::write(
            self.sec_key_path(name)?,
            sec_key.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)?,
        )?;

        std::fs::write(
            self.pub_key_path(name)?,
            pub_key.to_public_key_pem(rsa::pkcs8::LineEnding::LF)?,
        )?;

        Ok(())
    }

    pub fn sync(&self, remote: Option<String>) -> Result<(), Error> {
        let status = Command::new("git")
            .arg("add")
            .arg(".")
            .current_dir(self.data_dir()?)
            .status()?;

        if !status.success() {
            eprintln!(
                "Failed to add changes, go to {data_dir:?} to fix it",
                data_dir = self.config.data_dir
            );
            return Err(Error);
        }

        let _ = Command::new("git")
            .arg("commit")
            .arg("-m")
            .arg("Auto commit by ezpass")
            .arg("-s")
            .current_dir(self.data_dir()?)
            .status()?;

        let remote = remote.as_ref().map_or("origin", |s| s.as_str());

        let status = Command::new("git")
            .arg("pull")
            .arg(remote)
            .arg("--rebase")
            .current_dir(self.data_dir()?)
            .status()?;

        if !status.success() {
            eprintln!(
                "Failed to pull remote `{remote}', go to {data_dir:?} to fix it",
                data_dir = self.config.data_dir
            );
            return Err(Error);
        }

        let status = Command::new("git")
            .arg("push")
            .arg(remote)
            .current_dir(self.data_dir()?)
            .status()?;

        if !status.success() {
            eprintln!(
                "Failed to push to remote `{remote}', go to {data_dir:?} to fix it",
                data_dir = self.config.data_dir
            );
            return Err(Error);
        }

        Ok(())
    }
}

fn show_totp(key: &str) {
    let url = format!("otpauth://totp/new?secret={key}&issuer=ezpass");
    let totp = TOTP::from_url(url).unwrap();

    if unsafe { !stdout_isatty() } {
        let token = totp.generate_current().unwrap();
        println!("{token}");
        return;
    }

    for _ in 0..100 {
        let token = totp.generate_current().unwrap();

        print!(
            "\r{:02}/30 {}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs()
                % 30,
            token,
        );
        let _ = std::io::stdout().flush();

        std::thread::sleep(std::time::Duration::new(1, 0))
    }
}

unsafe fn stdout_isatty() -> bool {
    libc::isatty(libc::STDOUT_FILENO) == 1
}

unsafe fn stdin_isatty() -> bool {
    libc::isatty(libc::STDIN_FILENO) == 1
}

fn b2sum(name: &str) -> String {
    let mut hasher = blake2::Blake2b512::new();

    // write input message
    hasher.update(name);

    // read hash digest and consume hasher
    let digest = hasher.finalize();

    format!("{:x}", digest)
}

fn load_secret_message(path: &Path) -> Result<SecretMessage, Error> {
    Ok(serde_json::from_reader(std::fs::File::open(path)?)?)
}

fn dump_list(ordered_list: BTreeSet<String>, path: &Path) -> Result<(), Error> {
    serde_json::to_writer_pretty(std::fs::File::create(path)?, &ordered_list)?;
    Ok(())
}

fn load_list(path: &Path) -> Result<BTreeSet<String>, Error> {
    Ok(serde_json::from_reader(std::fs::File::open(path)?)?)
}
